<?php
/**
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * ProtocolMessage defines the base class for all protocol buffers.
 */
namespace google\net;

require_once 'google/appengine/runtime/proto/Decoder.php';
require_once 'google/appengine/runtime/proto/Encoder.php';
require_once 'google/appengine/runtime/proto/ProtocolBufferDecodeError.php';
require_once 'google/appengine/runtime/proto/ProtocolBufferEncodeError.php';

/**
 * The parent class of all protocol buffers.
 * Subclasses are automatically generated by php protocol buffer compiler.
 * Encoding methods can raise ProtocolBufferEncodeError if a value for an
 * integer or long field is too large, or if any required field is not set.
 * Decoding methods can raise ProtocolBufferDecodeError if they couldn't
 * decode correctly, or the decoded message doesn't have all required fields.
 */
abstract class ProtocolMessage {
  /**
   * Serializes the message and return it as a string.
   *
   * @throws ProtocolBufferEncodeError If the protocol buffer is not
   * initialized.
   *
   * @return string The serialized protocol buffer.
   */
  public function serializeToString() {
    $uninitialized = $this->checkInitialized();
    if ($uninitialized !== null) {
      throw new ProtocolBufferEncodeError(
        "Not initialized: " . $uninitialized);
    }
    return $this->serializePartialToString();
  }

  /**
   * Serializes a protocol buffer that might not have all of the required fields
   * set.
   *
   * @return string The serialized protocol buffer.
   */
  public function serializePartialToString() {
    $enc = new Encoder();
    $this->outputPartial($enc);
    $res = $enc->toString();

    # TODO: for debugging of the proto implementation, should be removed
    # once the implementation is stable. Performance hit seems to be less
    # than 10%.
    if ($this->byteSizePartial() !== strlen($res)) {
      throw new ProtocolBufferEncodeError(
        "Internal bug: Encoded size doesn't match predicted");
    }
    return $res;
  }

  /**
   * Fills the message with a protocol buffer parsed from the given input
   * string.
   *
   * @param string $s The string containing a serialized protocol buffer.
   *
   * @throws ProtocolBufferDecodeError If the result message is not correctly
   * initialized.
   */
  public function parseFromString($s) {
    // Reads data from the string 's'.
    // Raises a ProtocolBufferDecodeError if, after successfully reading
    // in the contents of 's', this protocol message is still not initialized.
    $this->clear();
    $this->mergeFromString($s);
  }

  /**
   * Fills the message with a protocol buffer parsed from the given input
   * string. Will not fail if the resulting protocol buffer is not fully
   * initialized.
   *
   * @param string $s The string containing a serialized protocol buffer.
   */
  public function parsePartialFromString($s) {
    // Reads data from the string 's'.
    // Does not enforce required fields are set.
    $this->clear();
    $this->mergePartialFromString($s);
  }

  /**
   * Like parseFromString, fills the message with a protocol buffer parsed from
   * the given input string.
   * 
   * @param string $s The string containing a serialized protocol buffer.
   *
   * @throws ProtocolBufferDecodeError If the result message is not correctly
   * initialized.
   */
  public function mergeFromString($s) {
    // Adds in data from the string 's'.
    // Raises a ProtocolBufferDecodeError if, after successfully merging
    // in the contents of 's', this protocol message is still not initialized.
    $this->mergePartialFromString($s);
    $uninitialized = $this->checkInitialized();
    if ($uninitialized !== null) {
      throw new ProtocolBufferDecodeError(
        "Not initialized: " . $uninitialized);
    }
  }

  /**
   * Like parsePartialFromString, fills the message with a protocol buffer
   * parsed from the given input string. Will not fail if the resulting protocol
   * buffer is not fully initialized.
   *
   * @param string $s The string containing a serialized protocol buffer.
   */

  public function mergePartialFromString($s) {
    // Merges in data from the string 's'.
    // Does not enforce required fields are set.
    $d = new Decoder($s, 0, strlen($s));
    $this->tryMerge($d);
  }

  /**
   * Copies data from another protocol buffer into this protocol buffer.
   *
   * @param mixed $pb The protocol buffer to copy from.
   */
  public function copyFrom($pb) {
    // copy data from another protocol buffer
    if ($pb === $this) {
      return;
    }
    $this->clear();
    $this->mergeFrom($pb);
  }

  /**
   * Checks if this protocol message has all of the requried fields initialized.
   *
   * @return bool true if all fields are initialized, false otherwise.
   */
  public function isInitialized() {
    $uninitializedFields = $this->checkInitialized();
    return $uninitializedFields === null;
  }

  public abstract function checkInitialized();
  public abstract function tryMerge($decoder);
  public abstract function outputPartial($encoder);
  public abstract function byteSizePartial();
  public abstract function clear();
  public abstract function mergeFrom($proto);
  public abstract function equals($proto);
  public abstract function shortDebugString();

  /**
   * Format protocol buffer as a text string for debugging.
   */
  protected static function debugFormatString($value) {
    $res = "";
    if (!empty($value)) {
      foreach (str_split($value) as $c) {
        $res .= ProtocolMessage::escapeByte($c);
      }
    }
    return '"' . $res . '"';
  }

  protected static function debugFormatDouble($value) {
    $val = "" . $value;
    if (strpos($val, "E") !== false) {
      return str_replace("E", "e", $val);
    }
    if (strpos($val, ".") === false) {
      $val .= ".0";
    }
    return $val;
  }

  protected static function lengthVarUint64($value) {
    if ($value < 0) {
      throw new ProtocolBufferEncodeError("Negative value");
    }
    if ($value <= 0x7f) {
      return 1;
    } elseif ($value <= 0x3fff) {
      return 2;
    } elseif ($value <= 0x1fffff) {
      return 3;
    } elseif ($value <= 0xfffffff) {
      return 4;
    } elseif (bccomp($value, "34359738367") <= 0) {
      # 0x7ffffffff
      return 5;
    } elseif (bccomp($value, "4398046511103") <= 0) {
      # 0x3ffffffffff
      return 6;
    } elseif (bccomp($value, "562949953421311") <= 0) {
      # 0x1ffffffffffff
      return 7;
    } elseif (bccomp($value, "72057594037927935") <= 0) {
      # 0xffffffffffffff
      return 8;
    } elseif (bccomp($value, "9223372036854775807") <= 0) {
      # 0x7fffffffffffffff
      return 9;
    } elseif (bccomp($value, "18446744073709551615") > 0) {
      # MAXUINT64
      throw new ProtocolBufferEncodeError("Value out of range: " . $value);
    } else {
      return 10;
    }
  }

  protected static function lengthVarInt64($value) {
    if ($value < 0) {
      if (bccomp($value, bcsub(0, bcpow(2,63))) < 0) {
        throw new ProtocolBufferEncodeError(
          "Value out of sint64 range: " . $value);
      }
      $value = bcadd($value, bcpow(2, 64));
    } elseif ($value > 2147483648 && bccomp($value, bcpow(2, 64)) >= 0) {
      throw new ProtocolBufferEncodeError(
        "Value out of sint64 range: " . $value);
    }
    return ProtocolMessage::lengthVarUint64($value);
  }

  protected static function lengthVarInt32($value) {
    return ProtocolMessage::lengthVarInt64($value);
  }

  protected static function lengthString($len) {
    return ProtocolMessage::lengthVarInt32($len) + $len;
  }

  protected static function debugFormatBool($b) {
    if ($b === true) {
      return "true";
    } else if ($b === false) {
      return "false";
    } else {
      return "???";
    }
  }

  protected static function debugFormatFloat($value) {
    return sprintf("%ff", $value);
  }

  protected static function debugFormatFixed32($value) {
    if ($value < 0) $value = bcadd($value, bcpow(2, 32));
    return ProtocolMessage::debugFormatFixed64($value);
  }

  protected static function debugFormatFixed64($value) {
    if ($value < 0) $value = bcadd($value, bcpow(2, 64));
    $res = "";
    do {
      $low = bcmod($value, 65536);
      $value = bcdiv($value, 65536);
      if ($value == 0) {
        if ($low != 0) {
          $res = sprintf("%x", $low) . $res;
        }
      } else {
        $res = sprintf("%04x", $low) . $res;
      }
    } while ($value != 0);

    if ("z" . $res === "z") {
      $res = "0";
    }

    return sprintf("0x%s", $res);
  }

  protected static function debugFormatInt32($value) {
    if ($value <= -2000000000 or $value >= 2000000000)
      return ProtocolMessage::debugFormatFixed32($value);
    return sprintf("%d", $value);
  }

  protected static function debugFormatInt64($value) {
    if (bccomp($value, "-20000000000000") <= 0
      or bccomp($value, "20000000000000")>= 0)
      return ProtocolMessage::debugFormatFixed64($value);
    return strval($value);
  }

  protected function checkProtoArray($arr) {
    if (sizeof($arr) == 0) {
      return;
    }
    // Quick approximate check that indexes of array are sane.
    $keys = array_keys($arr);
    if (end($keys) + 1 != sizeof($arr)) {
      throw new ProtocolBufferEncodeError("Proto array should not have holes");
    }
  }

  /**
   * Checks if two integers are equal.
   *
   * @param int $a The first integer to compare.
   * @param int $b The second integer to compare.
   *
   * @return bool True if the intergers are equal, false otherwise.
   */
  protected static function integerEquals($a, $b) {
    return ($a === $b) || (strval($a) === strval($b));
  }

  private static function escapeByte($c) {
    # Copied from python implementation:
    # For now we only escape the bare minimum to insure interoperabilty
    # and redability. In the future we may want to mimick the c++ behavior
    # more closely, but this will make the code a lot more messy.
    if ($c == "\n") return "\\n";  # optional escape
    if ($c == "\r") return "\\r";  # optional escape
    if ($c == "'") return "\\'";  # optional escape

    if ($c == "\"") return "\\\"";  # necessary escape
    if ($c == "\\") return "\\\\";  # necessary escape

    if ($c < "\x20" || $c >= "\x7F") {
      $o = unpack("C*", $c);
      return sprintf("\\%03o", $o[1]);
    }
    return $c;
  }
}
